6장. 문자열 다루기 기초
4장에서 문자열을 가볍게 소개했다. 이 장에서는 Go 의 문자열이 내부적으로 어떻게 생겼는지, 그리고 한글 같은 다국어 문자를 다룰 때 무엇을 조심해야 하는지 본다.
목표:
- 문자열이 “바이트의 나열” 이라는 점 이해하기
- 영문과 한글의 길이 차이를 설명할 수 있게 되기
byte와rune의 쓰임새 구분하기
6.1 문자열의 본질
불변 (immutable)
Go 의 문자열은 한 번 만들어지면 내용을 바꿀 수 없다.
s := "hello"
s[0] = 'H' // 컴파일 에러
문자열의 일부 글자를 바꾸고 싶다면 새 문자열을 만들어 변수에 다시 대입해야 한다.
s = "Hello" // 새 문자열을 대입하는 건 가능
“변수가 가리키는 문자열을 통째로 교체” 하는 건 자유다. 다만 기존 문자열의 내부 글자만 살짝 바꾸는 건 안 된다.
바이트의 나열, UTF-8 인코딩
Go 에서 문자열은 사실 바이트들의 묶음 이다. 그리고 그 바이트들은 UTF-8 로 인코딩돼 있다.
- 영문, 숫자, 기호 같은 ASCII 문자는 1바이트
- 한글, 한자, 이모지 같은 문자는 2~4바이트
이 사실이 곧 이번 장의 함정들로 이어진다.
큰따옴표 vs 백틱
문자열 리터럴은 두 가지 방법으로 적을 수 있다.
s1 := "Hello\n World" // 큰따옴표
s2 := `Hello\n World` // 백틱 (raw string)
큰따옴표 "..." | 백틱 `...` |
|---|---|
이스케이프 문자 해석 (\n, \t 등) | 이스케이프 해석 안 함 |
| 한 줄만 가능 | 여러 줄 가능 |
s1 := "줄1\n줄2" // "줄1" + 줄바꿈 + "줄2"
s2 := `줄1\n줄2` // 그대로 "줄1\n줄2"
s3 := `여러
줄에 걸친
문자열`
정규식, JSON 템플릿, SQL 같이 역슬래시가 많이 나오는 문자열은 백틱이 편하다.
6.2 연결과 비교
+ 로 연결
문자열은 + 로 이어붙일 수 있다.
first := "Hello"
last := "World"
msg := first + ", " + last + "!"
fmt.Println(msg) // Hello, World!
+= 로 누적도 가능하다.
s := ""
s += "안녕"
s += "하세요"
fmt.Println(s) // 안녕하세요
짧은 연결은
+로 충분하지만, 반복문 안에서 수백 번 이어붙여야 한다면strings.Builder가 훨씬 효율적이다. (26장에서)
비교
문자열도 5장의 비교 연산자를 그대로 쓴다.
fmt.Println("apple" == "apple") // true
fmt.Println("apple" != "Apple") // true (대소문자 다름)
fmt.Println("apple" < "banana") // true (사전식)
비교는 바이트 단위로 이뤄진다. 한글도 마찬가지지만, “가나다 순으로 정확히 정렬되느냐” 는 더 복잡한 이야기다. 지금은 “같은가 다른가” 비교 정도만 안전하다고 보면 된다.
6.3 길이 구하기
len() 은 바이트 단위
문자열의 길이는 len() 함수로 구한다.
fmt.Println(len("hello")) // 5
여기까지는 직관과 같다. 하지만 한글이 들어가면 결과가 달라진다.
fmt.Println(len("안녕")) // 6
한글 한 글자가 UTF-8 에서 3바이트를 차지하기 때문이다.
"안녕" 은 2글자지만 6바이트다.
len(s)은 글자 수가 아니라 바이트 수다.
영문 한 글자는 1바이트, 한글 한 글자는 3바이트라는 점을 잊지 말자.
글자 수가 필요할 땐?
진짜 문자 단위 길이는 []rune 으로 변환해서 구한다.
s := "안녕Go"
fmt.Println(len(s)) // 8 (3 + 3 + 1 + 1)
fmt.Println(len([]rune(s))) // 4 (안, 녕, G, o)
rune 의 정체는 잠시 뒤 6.5 절에서 다룬다.
6.4 인덱싱과 슬라이싱
s[i] 는 byte 를 돌려준다
문자열에 대괄호로 인덱스를 주면 그 위치의 바이트 를 돌려준다.
s := "hello"
fmt.Println(s[0]) // 104 (소문자 'h' 의 ASCII 코드값)
문자열을 인덱싱하면 글자가 아니라 숫자가 나오는 게 처음엔 어색하다. “인덱싱 = 바이트 꺼내기” 라고 기억하자.
문자처럼 보고 싶다면 변환이 필요하다.
fmt.Println(string(s[0])) // "h"
슬라이싱
s[i:j] 형태로 부분 문자열을 잘라낼 수 있다.
s := "Hello, World!"
fmt.Println(s[0:5]) // "Hello"
fmt.Println(s[7:12]) // "World"
fmt.Println(s[:5]) // "Hello" (앞부터 5바이트)
fmt.Println(s[7:]) // "World!" (7번째부터 끝까지)
여기서도 단위는 글자가 아니라 바이트 다.
한글 인덱싱의 함정
영문 문자열은 한 글자 = 한 바이트라 인덱싱이 직관적이다. 하지만 한글은 그렇지 않다.
s := "안녕"
fmt.Println(s[0]) // 236 (한글 첫 글자의 첫 바이트)
fmt.Println(s[1]) // 149 (둘째 바이트)
fmt.Println(s[2]) // 136 (셋째 바이트, 여기까지가 '안' 한 글자)
s[0] 이 '안' 글자 통째가 아니다.
'안' 을 구성하는 3바이트 중 첫 번째 바이트일 뿐이다.
그래서 한글 문자열을 잘못 슬라이싱하면 글자가 깨진다.
s := "안녕하세요"
fmt.Println(s[0:1]) // 깨진 문자
fmt.Println(s[0:3]) // "안"
[0:3] 처럼 글자 경계에 딱 맞춰 잘라야 온전한 글자가 된다.
이게 다음 6.5 절에서 rune 이 필요한 이유로 이어진다.
6.5 byte 와 rune
두 타입의 정체
4장에서 살짝 봤다.
byte=uint8(1바이트 정수)rune=int32(4바이트 정수, 유니코드 코드 포인트)
각각 다른 목적으로 쓴다.
| 타입 | 용도 |
|---|---|
byte | “한 바이트” 를 다룰 때 |
rune | “한 글자(유니코드)” 를 다룰 때 |
[]rune 으로 변환
문자열을 글자 단위로 다루려면 []rune 으로 변환한다.
s := "안녕Go"
bs := []byte(s)
rs := []rune(s)
fmt.Println(len(bs)) // 8 (바이트 개수)
fmt.Println(len(rs)) // 4 (글자 개수)
fmt.Println(string(rs[0])) // "안"
fmt.Println(string(rs[1])) // "녕"
fmt.Println(string(rs[2])) // "G"
fmt.Println(string(rs[3])) // "o"
rs[0] 은 '안' 한 글자 전체에 해당하는 코드 포인트(정수) 다.
string() 으로 다시 감싸면 우리가 아는 문자가 된다.
글자 단위 작업 패턴
한글이 섞인 문자열을 다룰 때는 보통 이렇게 한다.
s := "안녕하세요"
rs := []rune(s)
fmt.Println(len(rs)) // 5 (글자 수)
fmt.Println(string(rs[:3])) // "안녕하"
fmt.Println(string(rs[2:])) // "하세요"
“바이트로 다룰지, 글자로 다룰지” 만 의식하면 한글 처리도 어렵지 않다.
range 로 글자 순회 (짧은 언급)
반복문 for ... range 로 문자열을 돌면
바이트가 아니라 rune 단위 로 순회한다.
for i, r := range "안녕Go" {
fmt.Println(i, string(r))
}
for 문 자체는 다음 8장에서 본격적으로 다룬다.
지금은 “range 로 돌리면 글자 단위” 라는 것만 기억해 두자.
6.6 정리
- 문자열은 불변 이며 UTF-8 바이트의 나열 이다
- 큰따옴표는 이스케이프 해석, 백틱은 원문 그대로(raw)
- 연결은
+, 비교는==,<등으로 한다 len(s)은 바이트 수 다 — 한글 한 글자는 3바이트s[i]인덱싱은 바이트 를 돌려준다- 글자 단위로 다루려면
[]rune으로 변환
이 정도면 일상적인 문자열 처리는 가능하다.
검색, 치환, 분리 같은 본격적인 기능은 strings 패키지에 들어 있다.
표준 라이브러리를 다루는 27장에서 다시 만난다.
다음 장에서는 그동안 무심코 써 온 fmt.Println 의 정체를 들여다본다.
출력 형식을 마음대로 조절하고, 사용자 입력도 받아 본다.